﻿# define _CRT_SECURE_NO_WARNINGS 1
什么是结构体？

结构体是一种用户定义的数据类型，它允许用户根据需要组合不同类型的变量。

struct Student
{
	char name[20];
	int age;
	float score;
};

结构体通过`struct`关键字来定义，它允许将多个不同类型的数据元素组合在一起，这些数据元素称为结构体的成员。

结构体类型的声明和创建
1. 结构体类型的声明

struct 结构体类型标签名
{
	成员声明1;
	成员声明2;
	...
};//分号不能丢

例如：

struct Student//此时只是声明了 Student 类型，
{				//但还没有创建任何 Student 类型的变量。
	int id;
	char name[20];
	float score;
};//分号不能丢

2. 创建结构体类型的变量的语法如下：

struct 结构体类型名 变量名;

例如：

创建一个名为student1的学生结构体变量：
struct Student student1;

也可以在声明结构体类型的同时创建变量：
struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
struct Stu s1;
struct Stu s2;


3.typedef关键字来为结构体类型定义别名，然后创建结构体变量：

// 首先定义一个结构体类型
struct Student
{
	int id;
	char name[20];
};

int main()
{

	// 使用typedef为Student结构体类型定义一个别名StudentType
	typedef struct Student StudentType;

	// 使用原结构体类型定义变量
	struct Student st1;

	// 使用新的类型别名定义变量 
	StudentType st2;

	// 访问结构体成员
	st1.id = 1001;
	st2.id = 1002;

	return 0;
}

typedef struct Student StudentType可以理解使用typedef把 struct Student重新取了一个名字 StudentType，此时这个 StudentTye就是一个类型，比如：int a; 这个StudentTye就相当于int， StudentType st2;
当然，这个 struct Student也可以理解为 int，所以也可以这么用 struct Student st1;

结构体变量的初始化
结构体变量的初始化主要有两种方式：

按照默认顺序初始化：
默认情况下，结构体成员的初始化顺序与它们在结构体定义中的顺序相同。

例如：

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
}p1;
struct Stu s1 = { "asenyaozixin",11,"男","2023012018" };
//定义结构体变量s2
指定顺序初始化：
可以通过在列表中指定成员名来指定成员的初始化顺序：
例如：

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
struct Stu s2 = { .age = 66,.id = "2023001001",.name = "ahuibuyiban",.sex = "nv" };

完整示例且打印：

struct Stu
{
	char name[20];//名字
	int age;//年龄
	char sex[5];//性别
	char id[20];//学号
};
int main()
{
	struct Stu s1 = { "asenyaozixin",11,"nan","2023012018" };//按照默认顺序初始化
	struct Stu s2 = { .age = 66,.id = "2024001001",.name = "ahuibuyiban",.sex = "nv" };//指定顺序初始化
	printf("%s %d %s %s\n", s1.name, s1.age, s1.sex, s1.id);
	printf("%s %d %s %s\n", s2.name, s2.age, s2.sex, s2.id);

	return 0;
}


结构成员访问操作符
结构成员访问操作符用于访问结构体中的成员变量。

结构体成员的直接访问

结构体成员的直接访问----点操作符(.)
使⽤⽅式：结构体变量.成员名
使用点操作符可以访问结构的普通成员，例如：

struct Stu
{
	char name[20];
	int age;
	float score;
} s3 = { "熊大", 33, 66.0f }, s4 = { "熊二", 18, 100.0f };//全局变量

int main()
{
	struct Stu s1 = { "zhangsan", 20, 95.5f };//局部变量
	struct Stu s2 = { "lisi", 18, 87.5f };
	struct Stu s5 = { .score = 98.5f, .name = "hehe", .age = 18 };

	//. 结构成员访问操作符
	//结构体变量.成员名
	//
	printf("%s %d %f\n", s1.name, s1.age, s1.score);
	printf("%s %d %f\n", s2.name, s2.age, s2.score);
	printf("%s %d %f\n", s3.name, s3.age, s3.score);
	printf("%s %d %f\n", s4.name, s4.age, s4.score);
}


结构体成员的间接访问----箭头操作符(->)
使⽤⽅式：结构体指针->成员名
当结构体变量声明为结构体指针时，使用箭头操作符访问其成员：

struct Stu
{
	char name[20];
	int age;
	float score;
} s3 = { "熊大", 33, 66.0f }, s4 = { "熊二", 18, 100.0f };//全局变量

int main()
{
	struct Stu s1 = { "zhangsan", 20, 95.5f };//局部变量
	struct Stu s2 = { "lisi", 18, 87.5f };
	struct Stu s5 = { .score = 98.5f, .name = "hehe", .age = 18 };

	//结构体指针
	struct Stu* p1 = &s1;//取出s1的地址
	struct Stu* p2 = &s2;//取出s2的地址
	struct Stu* p3 = &s3;//取出s3的地址
	struct Stu* p4 = &s4;//取出s4的地址
	struct Stu* p5 = &s5;//取出s5的地址

	printf("%s %d %f\n", p1->name, p1->age, p1->score);
	printf("%s %d %f\n", p2->name, p2->age, p2->score);
	printf("%s %d %f\n", p3->name, p3->age, p3->score);
	printf("%s %d %f\n", p4->name, p4->age, p4->score);
	printf("%s %d %f\n", p5->name, p5->age, p5->score);

	//结构体指针->成员名

	return 0;
}

匿名结构体类型
匿名结构体类型就是没有给结构体类型起名字的结构体类型。

匿名结构体的定义方式：

struct
{
	成员1 数据类型;
	成员2 数据类型;
	...
} 变量名1, 变量名2, ...;
例如：

struct
{
	int a;
	char b;
	float c;
} x;

匿名结构体的特点是：

不需要给结构体起名字，定义时不指定结构体名称。

只能在定义它的代码块内使用，不能在其他地方再次使用这个匿名结构体类型。

思考：下⾯的两个结构在声明的时候省略掉了结构体标签（tag），然后主函数里的p = &x的代码合法吗 ?

struct
{
	int a;
	char b;
	float c;
} x;

struct
{
	int a;
	char b;
	float c;
} *p;


int main()
{
	p = &x;//?代码合法吗？
	return 0;
}
输出没问题但有警告：

警告：
编译器会把上⾯的两个声明当成完全不同的两个类型，所以是⾮法的。
匿名的结构体类型，如果没有对结构体类型重命名的话，基本上只能使⽤⼀次。

结构的⾃引⽤
结构的自引用指的是结构体内部包含自己类型的指针成员，通过这个指针可以实现结构体之间的引用关系。

⾃引⽤⽅式：

struct Node
{
	int data;
	struct Node* next;
};

// Node结构体包含一个指向Node结构体的指针next
// 通过next可以实现链表节点之间的引用关系
思考1：
在结构中包含⼀个类型为该结构本⾝的成员是否可以呢？
⽐如，定义⼀个链表的节点：
struct Node
{
	int data;
	struct Node next;
};
上述代码正确吗？如果正确，那 sizeof(struct Node) 是多少？
仔细分析，其实是不⾏的，因为⼀个结构体中再包含⼀个同类型的结构体变量，
这样结构体变量的⼤⼩就会⽆穷的⼤，是不合理的。
思考2：
在结构体⾃引⽤使⽤的过程中，夹杂了typedef对匿名结构体类型重命名，也容易引⼊问题，看看下⾯的代码，可⾏吗？
typedef struct
{
	int data;//存放数据
	Node* next;//存放写一个节点的地址
}Node;
int main()
{

	return 0;
}

分析：
首先使用typedef给前面匿名结构体起了别名Node，还不是类型，但是在typedef语句内，struct定义部分还没有结束，所以在struct内部使用Node声明next时，Node类型还未通typedef获得定义，仅仅是对匿名结构体的一个重命名，就提前使⽤Node类型来创建成员变量。

解决⽅案如下：定义结构体不要使⽤匿名结构体了
如下：
先定义结构体：

struct Node
{
	int data;
	struct Node* next;
}
再使用typedef给它起别名：

typedef struct Node Node;
或者一步完成：

typedef struct Node
{
	int data;
	struct Node* next;
} Node;



结构体实现位段
什么是位段
位段的声明和结构是类似的，有两个不同：
1. 位段的成员必须是 int、unsigned int 或signed int ，在C99中位段成员的类型也可以
选择其他类型。
2. 位段的成员名后边有⼀个冒号和⼀个数字。
⽐如：
struct A
{
    int _a : 2;
    int _b : 5;
    int _c : 10;
    int _d : 30;
};

A就是⼀个位段类型。
那位段A所占内存的⼤⼩是多少？
int main()
{
    printf("%d\n", sizeof(struct A));
    return 0;
}
位段的内存分配
1. 位段的成员可以是 int unsigned int signed int 或者是 char 等类型
2. 位段的空间上是按照需要以4个字节（ int ）或者1个字节（ char ）的⽅式来开辟的。
3. 位段涉及很多不确定因素，位段是不跨平台的，注重可移植的程序应该避免使⽤位段。
struct S
{
    char a : 3;
    char b : 4;
    char c : 5;
    char d : 4;
};

int main()
{
    struct S s = { 0 };
    s.a = 10;
    s.b = 8;
    s.c = 3;
    s.d = 4;
    printf("%d\n", sizeof(s));
    return 0;
}

位段的跨平台问题
1. int 位段被当成有符号数还是⽆符号数是不确定的。
2. 位段中最⼤位的数⽬不能确定。（16位机器最⼤16，32位机器最⼤32，写成27，在16位机器会
出问题。
3. 位段中的成员在内存中从左向右分配，还是从右向左分配标准尚未定义。
4. 当⼀个结构包含两个位段，第⼆个位段成员⽐较⼤，⽆法容纳于第⼀个位段剩余的位时，是舍弃
剩余的位还是利⽤，这是不确定的。
总结：
跟结构相⽐，位段可以达到同样的效果，并且可以很好的节省空间，但是有跨平台的问题存在。
4.4 位段的应⽤
下图是⽹络协议中，IP数据报的格式，我们可以看到其中很多的属性只需要⼏个bit位就能描述，这⾥
使⽤位段，能够实现想要的效果，也节省了空间，这样⽹络传输的数据报⼤⼩也会较⼩⼀些，对⽹络
的畅通是有帮助的。



4.5 位段使⽤的注意事项
位段的⼏个成员共有同⼀个字节，这样有些成员的起始位置并不是某个字节的起始位置，那么这些位
置处是没有地址的。内存中每个字节分配⼀个地址，⼀个字节内部的bit位是没有地址的。
所以不能对位段的成员使⽤ & 操作符，这样就不能使⽤scanf直接给位段的成员输⼊值，只能是先输⼊
放在⼀个变量中，然后赋值给位段的成员。
struct A
{
 int _a : 2;
 int _b : 5;
 int _c : 10;
 int _d : 30;
};

int main()
{
	struct A sa = { 0 };
	scanf("%d", &sa._b);//这是错误的

	//正确的⽰范
	int b = 0;
	scanf("%d", &b);
	sa._b = b;
	return 0;
}
